<template>
    <div>
        <h1>chrome浏览器垃圾机制与内存泄漏分析</h1>
       <h2>垃圾回收机制</h2>
       <p>通常情况下，垃圾数据回收分为手动回收和自动回收两种策略。</p>
       <p><strong>手动回收策略</strong>，何时分配内存、何时销毁内存都是由代码控制的。 </p>
       <p><strong>自动回收策略</strong>，产生的垃圾数据是由垃圾回收器来释放的，并不需要手动通过代码来释放。</p>

       <h3>JavaScript 中调用栈中的数据回收</h3>
       <p>JavaScript 引擎会通过向下移动 ESP(记录当前执行状态的指针) 来销毁该函数保存在栈中的执行上下文。</p>

       <h3>JavaScript 堆中的数据回收</h3>
       <p>在 V8 中会把堆分为新生代和老生代两个区域，新生代中存放的是生存时间短的对象，老生代中存放的生存时间久的对象。</p>
       <p>新生区通常只支持 1～8M 的容量，而老生区支持的容量就大很多了。对于这两块区域，V8 分别使用两个不同的垃圾回收器，以便更高效地实施垃圾回收。</p>
       <ul>
           <li>副垃圾回收器，主要负责新生代的垃圾回收。</li>
           <li>主垃圾回收器，主要负责老生代的垃圾回收。</li>
       </ul>
       <ol>
           不论什么类型的垃圾回收器，它们都有一套共同的执行流程。
           <li>第一步是标记空间中活动对象和非活动对象。所谓活动对象就是还在使用的对象，非活动对象就是可以进行垃圾回收的对象。</li>
           <li>第二步是回收非活动对象所占据的内存。其实就是在所有的标记完成之后，统一清理内存中所有被标记为可回收的对象。</li>
           <li>第三步是做内存整理。一般来说，频繁回收对象后，内存中就会存在大量不连续空间，我们把这些不连续的内存空间称为内存碎片，。当内存中出现了大量的内存碎片之后，如果需要分配较大连续内存的时候，就有可能出现内存不足的情况。所以最后一步需要整理这些内存碎片。(这步其实是可选的，因为有的垃圾回收器不会产生内存碎片).</li>
       </ol>

       <h3>老生代中的垃圾回收</h3>
       <p>老生代中用标记 - 清除（Mark-Sweep）的算法来处理。首先是标记过程阶段，标记阶段就是从一组根元素开始，递归遍历这组根元素(遍历调用栈)，在这个遍历过程中，能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据.然后在遍历过程中标记，标记完成后就进行清除过程。它和副垃圾回收器的垃圾清除过程完全不同，这个的清除过程是删除标记数据。</p>
       <p>清除算法后，会产生大量不连续的内存碎片。而碎片过多会导致大对象无法分配到足够的连续内存，于是又产生了标记 - 整理（Mark-Compact）算法，这个标记过程仍然与标记 - 清除算法里的是一样的，但后续步骤不是直接对可回收对象进行清理，而是让所有存活的对象都向一端移动，然后直接清理掉端边界以外的内存，从而让存活对象占用连续的内存块。</p>

        <h3>全停顿</h3>
        <p>由于 JavaScript 是运行在主线程之上的，一旦执行垃圾回收算法，都需要将正在执行的 JavaScript 脚本暂停下来，待垃圾回收完毕后再恢复脚本执行。我们把这种行为叫做全停顿。</p>
        <p>在 V8 新生代的垃圾回收中，因其空间较小，且存活对象较少，所以全停顿的影响不大，但老生代就不一样了。如果执行垃圾回收的过程中，占用主线程时间过久，主线程是不能做其他事情的。比如页面正在执行一个 JavaScript 动画，因为垃圾回收器在工作，就会导致这个动画在垃圾回收过程中无法执行，这将会造成页面的卡顿现象。</p>
        <p>为了降低老生代的垃圾回收而造成的卡顿，V8 将标记过程分为一个个的子标记过程，同时让垃圾回收标记和 JavaScript 应用逻辑交替进行，直到标记阶段完成，我们把这个算法称为增量标记（Incremental Marking）算法.</p>
        <p>使用增量标记算法，可以把一个完整的垃圾回收任务拆分为很多小的任务，这些小的任务执行时间比较短，可以穿插在其他的 JavaScript 任务中间执行，这样当执行上述动画效果时，就不会让用户因为垃圾回收任务而感受到页面的卡顿了。</p>
    
        <h2>内存泄漏</h2>
        <p>不再用到的内存，没有及时释放，就叫做内存泄漏（memory leak）。</p>
        <h3>内存泄漏发生的原因</h3>
        <h4>1. 缓存</h4>
        <p>有时候为了方便数据的快捷复用，我们会使用缓存,但是缓存必须有一个大小上限才有用。高内存消耗将会导致缓存突破上限，因为缓存内容无法被回收。</p>
        <h4>2. 计时器中引用没有清除</h4>
        <p>当浏览器队列消费不及时时，会导致一些作用域变量得不到及时的释放，因而导致内存泄漏。</p>
        <h4>3. 全局变量</h4>
        <p>除了常规设置了比较大的对象在全局变量中，还可能是意外导致的全局变量，如：</p>
        <img src="../assets/img/garbage-1.jpg">
        <p>在函数中，没有使用 var/let/const 定义变量，这样实际上是定义在window上面，变成了window.bar。再比如由于this导致的全局变量：</p>
        <img src="../assets/img/garbage-2.jpg">
        <p>这种函数，在window作用域下被调用时，函数里面的this指向了window,执行时实际上为window.bar=xxx,这样也产生了全局变量。</p>
        <h4>4. 计时器中引用没有清除</h4>
        <p>先看如下代码：</p>
        <img src="../assets/img/garbage-3.jpg">
        <p>这里定义了一个计时器，每隔1s把一些数据写到Node节点里面。但是当这个Node节点被删除后，这里的逻辑其实都不需要了，可是这样写，却导致了计时器里面的回调函数无法被回收，同时，someData里的数据也是无法被回收的。</p>
        <h4>5. 闭包</h4>
        <p>看以下这个闭包：</p>
        <img src="../assets/img/garbage-4.jpg">
        <p>每次调用 replaceThing ，theThing 会创建一个大数组和一个新闭包（someMethod）的新对象。同时，变量 unused 是一个引用 originalThing(theThing) 的闭包，闭包的作用域一旦创建，它们有同样的父级作用域，作用域是共享的。</p>
        <p>即 someMethod 可以通过 theThing 使用，someMethod 与 unused 分享闭包作用域，尽管 unused 从未使用，它引用的 originalThing 迫使它保留在内存中（防止被回收）。</p>
        <p>因此，当这段代码反复运行，就会看到内存占用不断上升，垃圾回收器（GC）并无法降低内存占用。</p>
        <p>本质上，闭包的链表已经创建，每一个闭包作用域携带一个指向大数组的间接的引用，造成严重的内存泄漏。</p>
        <h4>6. 事件监听</h4>
        <p>例如，Node.js 中 Agent 的 keepAlive 为 true 时，可能造成的内存泄漏。当 Agent keepAlive 为 true 的时候，将会复用之前使用过的 socket，如果在 socket 上添加事件监听，忘记清除的话，因为 socket 的复用，将导致事件重复监听从而产生内存泄漏。</p>

        <h3>内存泄漏的识别方法</h3>
        <h4>1、使用 Chrome 任务管理器实时监视内存使用 打开 chrome 浏览器</h4>
        <p>点击右上角主菜单，选择更多工具->任务管理器，这样就开启了任务管理器面板，然后再右键点击任务管理器的表格标题并启用 JavaScript使用的内存</p>
        <ol>
            <li>内存占用空间(Memory) 列表示原生内存。DOM 节点存储在原生内存中。如果此值正在增大，则说明正在创建 DOM 节点。</li>
            <li>JavaScript使用的内存(JavaScript Memory) 列表示 JS 堆。此列包含两个值。您感兴趣的值是实时数字（括号中的数字）。实时数字表示您的页面上的可到达对象正在使用的内存量。如果此数字在增大，要么是正在创建新对象，要么是现有对象正在增长。</li>
        </ol>
        <p>当你页面稳定下来之后，这两个的值还在上涨，你就可以查一查是否内存泄漏了。</p>
        <h4>2、利用chrome 时间轴记录可视化内存泄漏</h4>
        <p>Performance(时间轴)能够面板直观实时显示JS内存使用情况、节点数量、监听器数量等。</p>
        <p>打开 chrome 浏览器，调出调试面板(DevTools),点击Performance选项(低版本是Timeline)，勾选Memory复选框。一种比较好的做法是使用强制垃圾回收开始和结束记录。在记录时点击 Collect garbage 按钮 (强制垃圾回收按钮) 可以强制进行垃圾回收。所以录制顺序可以这样：开始录制前先点击垃圾回收-->点击开始录制-->点击垃圾回收-->点击结束录制。</p>
        <p>首先，从图中我们可以看出不同颜色的曲线代表的含义，这里主要关注JS堆内存、节点数量、监听器数量。鼠标移到曲线上，可以在左下角显示具体数据。在实际使用过程中，如果您看到这种 JS 堆大小或节点大小不断增大的模式，则可能存在内存泄漏。</p>
        <h4>3、使用堆快照发现已分离 DOM 树的内存泄漏</h4>
        <p>只有页面的 DOM 树或 JavaScript 代码不再引用 DOM 节点时，DOM 节点才会被作为垃圾进行回收。如果某个节点已从 DOM 树移除，但某些 JavaScript 仍然引用它，我们称此节点为“已分离”，已分离的 DOM 节点是内存泄漏的常见原因。</p>
        <p>调出调试面板，点击Memory，然后选择Heap Snapshot，然后点击进行录制。录制完成后，选中录制结果，在 Class filter 文本框中键入 Detached，搜索已分离的 DOM 树。</p>
        <p>旧版的面板，还会有颜色标注，黄色的对象实例表示它被JS代码引用，红色的对象实例表示被黄色节点引用的游离节点。新版本的不会有颜色标识。但是还是可以一个个来看，点开节点，可以看到下面的引用信息，上面可以看出，有个HTMLUListElement(ul节点)被window.detachedNodes引用。再结合代码，原来是没有加var/let/const声明，导致其成了全局变量,所以DOM无法释放。</p>
        <h4>4、按函数调查内存分配 打开面板</h4>
        <p>点击JavaScript Profiler,如果没看到这个选项，你可以点调试面板右上角的三个点，选择more tools，然后选择。</p>
        <p>ps: chrome 旧版的浏览器，这个功能在 Profiles 里面，点Record Allocation Profile即可.</p>
        <p>操作步骤：点start->在页面进行你要检测的操作->点stop。</p>
        <p>DevTools 按函数显示内存分配明细。默认视图为 Heavy (Bottom Up)，将分配了最多内存的函数显示在最上方，还有函数的位置，你可以看看是哪些函数占用内存较多。</p>

        <h3>避免内存泄漏的方法</h3>
        <ol>
            <li>少用全局变量，避免意外产生全局变量</li>
            <li>使用闭包要及时注意，有Dom元素的引用要及时清理</li>
            <li>计时器里的回调没用的时候要记得销毁</li>
            <li>为了避免疏忽导致的遗忘，我们可以使用 WeakSet 和 WeakMap结构，它们对于值的引用都是不计入垃圾回收机制的，表示这是弱引用。举个例子：<br>
const wm = new WeakMap();const element = document.getElementById('example');
wm.set(element, 'some information');
wm.get(element) // "some information"</li>
        </ol>
        <p>这种情况下，一旦消除对该节点的引用，它占用的内存就会被垃圾回收机制释放。Weakmap 保存的这个键值对，也会自动消失。</p>
        <p>基本上，如果你要往对象上添加数据，又不想干扰垃圾回收机制，就可以使用 WeakMap。</p>
    
    </div>
</template>